package helpers

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	f5_bigip "github.com/f5devcentral/f5-bigip-rest-go/bigip"
	"github.com/f5devcentral/f5-bigip-rest-go/utils"
)

type BIGIPHelper struct {
	username  string
	password  string
	ipAddress string
	port      int
	BC        *f5_bigip.BIGIPContext
}

func NewBIGIPHelper(ctx context.Context, username, password, ipAddress string, port int) *BIGIPHelper {
	url := fmt.Sprintf("https://%s:%d", ipAddress, port)
	bip := f5_bigip.New(url, username, password)
	return &BIGIPHelper{
		username:  username,
		password:  password,
		ipAddress: ipAddress,
		port:      port,
		BC:        &f5_bigip.BIGIPContext{BIGIP: *bip, Context: ctx},
	}
}

func (bh *BIGIPHelper) Check(kind, name, partition, subfolder string, properties map[string]interface{}) error {
	existings, err := bh.BC.Exist(kind, name, partition, subfolder)
	if err != nil {
		return fmt.Errorf("failed to get resources: %s", err)
	} else if existings == nil {
		return fmt.Errorf("empty response from bigip with p(%s) n(%s), f(%s), k(%s)", partition, name, subfolder, kind)
	} else {
		for k, props := range properties {
			if !utils.DeepEqual((*existings)[k], props) {
				return fmt.Errorf("expected: %v, actually: %v", props, (*existings)[k])
			}
		}
		return nil
	}
}

func (bh *BIGIPHelper) Exist(kind, name, partition, subfolder string) bool {
	slog := utils.LogFromContext(bh.BC.Context)
	existings, err := bh.BC.Exist(kind, name, partition, subfolder)
	if err != nil {
		slog.Warnf("failed to get resources: %s", err)
		return false
	} else if existings == nil {
		slog.Warnf("empty response from bigip with p(%s) n(%s), f(%s), k(%s)", partition, name, subfolder, kind)
		return false
	} else {
		// slog.Infof("existings: %v", *existings)
		return true
	}
}

func (bh *BIGIPHelper) NotExists(kind, name, partition, subfolder string) bool {
	slog := utils.LogFromContext(bh.BC.Context)
	existing, err := bh.BC.Exist(kind, name, partition, subfolder)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			slog.Infof("%s %s %s not exists", partition, subfolder, name)
			return true
		}
		slog.Warnf("failed to get /mgmt/tm/%s/~%s~%s~%s: %s", kind, partition, subfolder, name, err.Error())
		return false
	}
	return existing == nil
}

func (bh *BIGIPHelper) Get(kind, name, partition, subfolder string) (map[string]interface{}, error) {
	existings, err := bh.BC.Exist(kind, name, partition, subfolder)
	if err != nil {
		return *existings, fmt.Errorf("failed to get resources: %s", err)
	} else if existings == nil {
		return nil, fmt.Errorf("empty response from bigip with p(%s) n(%s), f(%s), k(%s)", partition, name, subfolder, kind)
	} else {
		return *existings, nil
	}
}

func (bh *BIGIPHelper) GetPoolMemberIPs(pool, partition, subfolder string) ([]string, error) {
	existings, err := bh.BC.Members(pool, partition, subfolder)
	if err != nil {
		return nil, err
	}

	rlt := []string{}
	for _, m := range existings {
		addr := m.(map[string]interface{})["address"].(string)
		rlt = append(rlt, addr)
	}

	return utils.Unified(rlt), nil
}

func (bh *BIGIPHelper) PartitionIsCreated(partition string) bool {
	existings, err := bh.BC.Exist("sys/folder", partition, "", "")
	if err != nil || existings == nil {
		return false
	} else {
		return true
	}
}

func (bh *BIGIPHelper) PartitionIsRemoved(partition string) bool {
	existings, err := bh.BC.Exist("sys/folder", partition, "", "")
	if err != nil || existings != nil {
		return false
	} else {
		return true
	}
}

func (bh *BIGIPHelper) LenOfPoolMembers(name, partition, subfolder string) int {
	slog := utils.LogFromContext(bh.BC.Context)
	if ms, err := bh.GetPoolMemberIPs(name, partition, subfolder); err != nil {
		slog.Warnf("failed to get pool members: %s/%s/%s", partition, subfolder, name)
		return -1
	} else {
		return len(ms)
	}
}

func (bh *BIGIPHelper) NodesInPartition(partition string) []string {
	slog := utils.LogFromContext(bh.BC.Context)
	existings, err := bh.BC.All("ltm/node")
	if err != nil {
		slog.Warnf("failed to get node: %s", err.Error())
		return nil
	} else if existings == nil {
		slog.Warnf("empty response from ltm/node")
		return nil
	} else {
		items := (*existings)["items"].([]interface{})
		rlt := []string{}
		for _, i := range items {
			n := i.(map[string]interface{})
			if n["partition"] == partition {
				rlt = append(rlt, n["address"].(string))
			}
		}
		return rlt
	}
}

func (bh *BIGIPHelper) Contains(url string, body map[string]interface{}) bool {
	slog := utils.LogFromContext(bh.BC.Context)
	u := url
	if strings.HasPrefix(u, "/mgmt/tm/") {
		u = strings.Replace(u, "/mgmt/tm/", "", 1)
	}

	existings, err := bh.BC.All(u)
	if err != nil {
		slog.Warnf("failed to get from %s: %s", url, err.Error())
		return false
	}

	if existings == nil {
		if body != nil {
			return false
		} else {
			return true
		}
	}

	// to eliminate error: because type miss match, expect: float64, actually: int
	var b1, b2 []byte
	b1, _ = json.Marshal(*existings)
	b2, _ = json.Marshal(body)

	var j1, j2 map[string]interface{}
	json.Unmarshal(b1, &j1)
	json.Unmarshal(b2, &j2)

	s, m := Contains(j1, j2)
	if !s {
		slog.Warnf("Contains returns false, because %s", m)
	}
	return s
}

func (bh *BIGIPHelper) UrlNotFound(url string) bool {
	slog := utils.LogFromContext(bh.BC.Context)
	existing, err := bh.BC.Exist(url, "", "", "")
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			slog.Infof("%s not exists", url)
			return true
		}
		return false
	}
	return existing == nil
}

func (bh *BIGIPHelper) GrabPartition(partitions []string, rootKinds []string, results map[string]map[string]interface{}) error {
	if len(partitions) == 0 {
		return nil
	}

	slog := utils.LogFromContext(bh.BC.Context)
	bc := bh.BC

	for _, kind := range rootKinds {
		slog.Debugf("kind: %s", kind)
		// the uri used in curl in terminal should be quota-ed as:
		// 	curl -k -u admin:xyz 'https://bigip.url.dev/mgmt/tm/ltm/virtual?$filter=partition+eq+p1'
		// notice the above "'" pair.
		// otherwise, curl would fail to utilize 'partition' filter.
		res, err := bc.All(kind)
		if err != nil {
			if strings.Contains(err.Error(), "was not found") {
				return nil
			} else {
				return err
			}
		}
		if _, f := (*res)["items"].([]interface{}); !f {
			return nil
		}
		for _, item := range (*res)["items"].([]interface{}) {
			if isLink(item) {
				ik := getKindFromLink(item)
				if !kindIsIncluded(ik) {
					slog.Debugf("skip kind: %s", ik)
					continue
				}
				err := bh.GrabPartition(partitions, []string{ik}, results)
				if err != nil {
					slog.Warnf("failed to request %s: %s", ik, err.Error())
					continue
				}
			} else {
				var r map[string]interface{}
				if err := utils.UnmarshalJson(item, &r); err != nil {
					return err
				}
				if name, fn := r["name"]; fn && name != "" {
					if _, fp := r["partition"]; fp {
						pt := r["partition"].(string)
						if utils.Contains(partitions, pt) {
							slog.Infof(" -> res : %s %s %s", kind, pt, name)
							if _, fpt := results[pt]; !fpt {
								results[pt] = map[string]interface{}{}
							}

							subPath := ""
							if sp, ff := r["subPath"]; ff {
								subPath = sp.(string)
							}
							if _, fr := results[pt][subPath]; !fr {
								results[pt][subPath] = map[string]interface{}{}
							}

							results[pt][subPath].(map[string]interface{})[kind+"/"+name.(string)] = r
						}
					} else {
						slog.Debugf("not found partition from resource %s of kind %s", name, kind)
					}
				}
			}
		}
	}
	return nil
}

func kindIsIncluded(kind string) bool {
	for _, k := range f5_bigip.ResOrder {
		if strings.HasPrefix(k, kind) {
			return true
		}
		krex := regexp.MustCompile(k)
		if krex.MatchString(kind) {
			return true
		}
	}
	return false
}
